package cn.langpy.disroute.core;

import cn.langpy.disroute.annotation.Route;
import cn.langpy.disroute.config.SpringContext;
import cn.langpy.disroute.exception.InvalidContextException;
import cn.langpy.disroute.exception.InvalidRoutePathException;
import cn.langpy.disroute.exception.NotInitRouteException;
import com.alibaba.fastjson.JSONObject;
import org.reflections.Reflections;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;

/**
 * Chang Zhang
 */
public class RouteContext {
    public static Logger log = Logger.getLogger(RouteContext.class.toString());

    static ConcurrentHashMap<String, Object> instanceMap = new ConcurrentHashMap();
    static ConcurrentHashMap<String, Method> methodMap = new ConcurrentHashMap();
    static ConcurrentHashMap<String, Boolean> methodAsyncMap = new ConcurrentHashMap();
    static ConcurrentHashMap<String, Boolean> interfaceMap = new ConcurrentHashMap();
    static ConcurrentHashMap<String, String> keyMap = new ConcurrentHashMap();

    static volatile boolean initFlag = false;

    protected static boolean isInit() {
        return initFlag;
    }

    protected static void init() {
        init("");
    }

    public static void close() {
        if (RoutePool.isFree()) {
            instanceMap.clear();
            methodMap.clear();
            keyMap.clear();
            RoutePool.closePool();
        }
        initFlag = false;
    }

    protected synchronized static void init(String packagePath) {
        if (isInit()) {
            return;
        }
        Reflections reflections = null;
        if (packagePath != null && packagePath.length() > 1) {
            reflections = new Reflections(packagePath);
        } else {
            reflections = new Reflections();
        }
        Set<Class<?>> services = reflections.getTypesAnnotatedWith(Route.class);
        for (Class<?> service : services) {
            Route contextRoute = service.getAnnotation(Route.class);
            if (null == contextRoute || null == contextRoute.value()) {
                continue;
            }
            String context = contextRoute.value();
            if (null == context || context.length() < 1) {
                context = "/".concat(service.getSimpleName());
            } else if (!context.startsWith("/")) {
                context = "/".concat(context);
            }

            Object instance = initInstance(service);
            if (instance instanceof RouteInterface) {
                String[] pathSplit = context.split("/");
                if (pathSplit.length != 3) {
                    throw new InvalidRoutePathException("error to create " + service.getSimpleName() + ".java with @Route ,please refer to @Route(\"/key/value\")");
                }
                String simpleName = getSuperClass(service).getSimpleName();
                context = "/".concat(simpleName).concat(context);
                Method onRoute = null;
                try {
                    onRoute = service.getMethod("onRoute", Object.class);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
                RouteContext.methodMap.put(context, onRoute);
                RouteContext.methodAsyncMap.put(context, contextRoute.async());
                RouteContext.keyMap.put("/".concat(simpleName), pathSplit[1]);
                RouteContext.interfaceMap.put("/".concat(simpleName), true);
            } else {
                Method[] methods = service.getDeclaredMethods();
                for (Method method : methods) {
                    Route routePath = method.getDeclaredAnnotation(Route.class);
                    if (null == routePath || null == routePath.value()) {
                        continue;
                    }

                    String path = routePath.value();
                    String[] pathSplit = path.split("/");
                    if (pathSplit.length != 3) {
                        throw new InvalidRoutePathException("error @Route,please refer to @Route(\"/key/value\")");
                    }

                    String key = "";
                    key = pathSplit[1];
                    RouteContext.methodMap.put(context + path, method);
                    RouteContext.keyMap.put(context, key);
                    RouteContext.interfaceMap.put(context, false);
                    RouteContext.methodAsyncMap.put(context + path, routePath.async());
                }
            }
            if (RouteContext.instanceMap.containsKey(context)) {
                continue;
            }
            RouteContext.instanceMap.put(context, initInstance(service));
            log.info("register @Route for " + service.getSimpleName());
        }
        initFlag = true;
    }

    private static Class<?> getSuperClass(Class<?> service) {
        Class<?>[] interfaces = service.getInterfaces();
        if (interfaces.length > 0) {
            return interfaces[0];
        }
        throw new InvalidRoutePathException("error to create " + service.getSimpleName() + ".java with @Route ,please implements an interface from RouteInterface!");
    }


    private static Object initInstance(Class<?> service) {
        Object instance = null;
        try {
            if (isSpring()) {
                Component component = service.getAnnotation(Component.class);
                if (component == null) {
                    instance = service.newInstance();
                } else {
                    instance = SpringContext.applicationContext.getBean(service);
                }
            } else {
                instance = service.newInstance();
            }
        } catch (InstantiationException e) {
            log.severe("can not register @Route for " + service.getSimpleName());
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            log.severe("can not register @Route for " + service.getSimpleName());
            e.printStackTrace();
        }
        return instance;
    }

    private static boolean isSpring() {
        try {
            Thread.currentThread().getContextClassLoader().loadClass("org.springframework.stereotype.Component");
        } catch (ClassNotFoundException e) {
            return false;
        }
        return true;
    }


    private static String getKey(String context) {
        String key = RouteContext.keyMap.get("/".concat(context));
        if (key == null) {
            throw new InvalidContextException();
        }
        return key;
    }

    private static Object getValue(String key, Object param) {
        Object keyValue = null;
        if (param instanceof String || param instanceof Integer || param instanceof Double || param instanceof Float) {
            keyValue = param;
        } else if (param instanceof JSONObject) {
            JSONObject map = (JSONObject) param;
            keyValue = map.get(key);
        } else if (param instanceof Map) {
            Map map = (Map) param;
            keyValue = map.get(key);
        } else {
            Field[] fields = param.getClass().getDeclaredFields();
            Optional<Field> keyFieldOption = Arrays.stream(fields).filter(name -> name.getName().equals(key)).findFirst();
            keyFieldOption.orElseThrow(() -> new InvalidRoutePathException());
            Field keyField = keyFieldOption.get();
            keyField.setAccessible(true);
            try {
                keyValue = keyField.get(param);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return keyValue;
    }

    private static Method getMethod(String context, String key, Object keyValue) {
        Method method = RouteContext.methodMap.get("/".concat(context).concat("/").concat(key).concat("/").concat(keyValue + ""));
        if (method == null) {
            method = RouteContext.methodMap.get("/".concat(context).concat("/").concat(key).concat("/*"));
            if (method == null) {
                throw new RuntimeException(String.format("can not find a method with @Route(\"/%s/%s\"),please create it!", key, keyValue));
            }
        }
        return method;
    }

    private static <T> T getResult(Object instance, Method method, Object param) {
        T result = null;
        Object resultObject = null;
        try {
            resultObject = method.invoke(instance, param);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        if (resultObject != null) {
            result = (T) resultObject;
        }
        return result;
    }

    public static <R> R dispatch(String context, Object param,RouteCallback callback) {
        if (!isInit()) {
            throw new NotInitRouteException();
        }
        RouteResult routeResult = new RouteResult<Object,R>();
        routeResult.setParam(param);
        routeResult.setStartTime(LocalDateTime.now());
        routeResult.setFiltered(false);
        String key = getKey(context);
        Object keyValue = getValue(key, param);
        Method method = getMethod(context, key, keyValue);
        Object instance = getInstance(context, key, keyValue);
        if (isAsync(context, key, keyValue)) {
            Object finalInstance = instance;
            RoutePool.getPool().execute(() -> {
                R result = getResult(finalInstance, method, param);
                if (callback!=null) {
                    RoutePool.getPool().execute(()->{
                        routeResult.setResult(result);
                        routeResult.setEndTime(LocalDateTime.now());
                        callback.callback(routeResult);
                    });
                }

            });
        } else {
            R result = getResult(instance, method, param);
            if (callback!=null) {
                RoutePool.getPool().execute(()->{
                    routeResult.setResult(result);
                    routeResult.setEndTime(LocalDateTime.now());
                    callback.callback(routeResult);
                });
            }
            return result;
        }
        return null;
    }

    private static boolean isAsync(String context, String key, Object value) {
        Boolean b = methodAsyncMap.get("/".concat(context).concat("/").concat(key).concat("/").concat(value + ""));
        if (b==null) {
            b = methodAsyncMap.getOrDefault("/".concat(context).concat("/").concat(key).concat("/*"),false);
        }
        return b.booleanValue();
    }

    private static Object getInstance(String context, String key, Object value) {
        Object instance = null;
        if (interfaceMap.get("/".concat(context)).booleanValue()) {
            instance = RouteContext.instanceMap.get("/".concat(context).concat("/").concat(key).concat("/").concat(value + ""));
            if (instance==null) {
                instance = RouteContext.instanceMap.get("/".concat(context).concat("/").concat(key).concat("/*"));
            }
        } else {
            instance = RouteContext.instanceMap.get("/".concat(context));
        }
        return instance;
    }

    public static <R> R dispatchAsync(Executor executor,String context, Object param,RouteCallback callback) {
        if (!isInit()) {
            throw new NotInitRouteException();
        }
        RouteResult routeResult = new RouteResult<Object,R>();
        routeResult.setParam(param);
        routeResult.setStartTime(LocalDateTime.now());
        routeResult.setFiltered(false);
        String key = getKey(context);
        Object keyValue = getValue(key, param);
        Method method = getMethod(context, key, keyValue);
        Object instance = getInstance(context, key, keyValue);
        Object finalInstance = instance;
        executor.execute(() -> {
            R result = getResult(finalInstance, method, param);
            if (callback!=null) {
                RoutePool.getPool().execute(()->{
                    routeResult.setResult(result);
                    routeResult.setEndTime(LocalDateTime.now());
                    callback.callback(routeResult);
                });
            }
        });
        return null;
    }
}
